﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace XmlSpecParser
{
	public class DotNetFileWriter
	{
		public class Options
		{
			public string Prefix { get; set; }

			public string OutputPath { get; set; }

			public string StaticFileName { get; set; }

            public string StaticClassName { get; set; }

            public string EnumsFileName { get; set; }

			public string NativeFunctionsFileName { get; set; }

			public string NativeFunctionsClassName { get; set; }

            public string NativeFunctionsEnumFileName { get; set; }

            public string NativeFunctionsEnumClassName { get; set; }

			public string DotNetFunctionsFileName { get; set; }

			public string DotNetClassName { get; set; }
		}

		public void WriteFiles(DotNetApiData api, Options options)
		{
			this.WriteStaticFile(api, options);
            this.WriteEnumFile(api, options);
            this.WriteNativeFunctionsFile(api, options);
            this.WriteNativeFunctionsEnumFile(api, options);
			this.WriteDotNetFunctionsFile(api, options);
		}

		private void WriteStaticFile(DotNetApiData api, Options options)
		{
			string fileName = Path.Combine(options.OutputPath, options.StaticFileName);
			using (StreamWriter file = new StreamWriter(File.Open(fileName, FileMode.Create)))
			{
				file.WriteLine("using System;");
				file.WriteLine();
				file.WriteLine("namespace GLDotNet");
				file.WriteLine("{");

                file.WriteLine("\tpublic static partial class {0}", options.StaticClassName);
                file.WriteLine("\t{");

				foreach (var @enum in api.Enums)
				{
					file.WriteLine("\t\t/// <summary>");
					file.WriteLine("\t\t/// {0}", @enum.OriginalName);
					file.WriteLine("\t\t/// </summary>");
					file.WriteLine("\t\tpublic const {0} {1} = {2};", @enum.Type, @enum.Name, @enum.Value);
					file.WriteLine();
				}

                file.WriteLine("\t}");
                file.WriteLine();
				file.WriteLine("}");
			}
		}

        private void WriteEnumFile(DotNetApiData api, Options options)
        {
            if (api.EnumGroups.Count == 0)
                return;

            string fileName = Path.Combine(options.OutputPath, options.EnumsFileName);
            using (StreamWriter file = new StreamWriter(File.Open(fileName, FileMode.Create)))
            {
                file.WriteLine("using System;");
                file.WriteLine();
                file.WriteLine("namespace GLDotNet");
                file.WriteLine("{");

                foreach (var enumGroup in api.EnumGroups)
                {                    
                    if (enumGroup.IsFlags)
                        file.WriteLine("\t[Flags]");

                    file.WriteLine("\tpublic enum {0} : uint", enumGroup.Name);
                    file.WriteLine("\t{");

                    foreach (var @enum in enumGroup.Enums)
                    {
                        file.WriteLine("\t\t/// <summary>");
                        file.WriteLine("\t\t/// {0}", @enum.OriginalName);
                        file.WriteLine("\t\t/// </summary>");
                        file.WriteLine("\t\t{0} = {1},", @enum.Name, @enum.Value);
                        file.WriteLine();
                    }

                    file.WriteLine("\t}");
                    file.WriteLine();
                }

                file.WriteLine("}");
            }
        }

        private void WriteNativeFunctionsEnumFile(DotNetApiData api, Options options)
        {
            string fileName = Path.Combine(options.OutputPath, options.NativeFunctionsEnumFileName);
            using (StreamWriter file = new StreamWriter(File.Open(fileName, FileMode.Create)))
            {
                file.WriteLine("using System;");
                file.WriteLine();
                file.WriteLine("namespace GLDotNet");
                file.WriteLine("{");

                file.WriteLine("\tpublic enum {0}", options.NativeFunctionsEnumClassName);
                file.WriteLine("\t{");

                foreach (var function in api.Functions.Select(x => x.NativeName).Distinct())
                {
                    file.WriteLine("\t\t{0},", function);
                    file.WriteLine();                    
                }

                file.WriteLine("\t}");

                file.WriteLine("}");
            }
        }
				
		private string GetSignature(string prefix, DotNetFunctionData function, bool useDotNetStyle)
		{
			string signature = prefix;

			string returnType = function.NativeReturnType;

			if (returnType == "GLenum" || returnType == "GLbitfield")
				returnType = "uint";

			string functionName = (useDotNetStyle ? function.DotNetName : function.NativeName);

			signature += returnType + " " + functionName;

			if (useDotNetStyle && function.ShouldUseGenerics)
				signature += "<T>";

			signature += "(";

			int i = 0;
			foreach (var param in function.Params)
			{
                if (!useDotNetStyle)
                {
                    if (param.IsOutput)
                        signature += "[Out] ";
                }
                else
                {
                    if (param.ShouldUseRef)
                    {
                        signature += "ref ";
                    }
                    else if (param.ShouldUseOut)
                    {
                        signature += "out ";
                    }
                }
                
				signature += (useDotNetStyle ? param.DotNetType : param.NativeType) + " " + param.Name;

				if (i < function.Params.Count - 1)
					signature += ", ";

				i++;
			}

			signature += ")";

			if (useDotNetStyle && function.ShouldUseGenerics)
				signature += " where T : struct";

			return signature;
		}

		private string GetNativeDeclaration(string padding, DotNetFunctionData function, bool asPublic)
		{
			string header = padding + "[System.Runtime.InteropServices.DllImport(Library, EntryPoint=\"" + function.OriginalName + "\", ExactSpelling=true)]" + Environment.NewLine;

			string prefix = string.Empty;

			if (asPublic)
				prefix = "public ";
			else
				prefix = "internal ";

			prefix += "static extern ";

			if (function.IsUnsafe)
				prefix += "unsafe ";
							
			string signature = this.GetSignature(padding + prefix, function, false) + ";";

			return header + signature;
		}

		private string GetDelegateDeclaration(string padding, DotNetFunctionData function)
		{
			string prefix = "internal delegate ";

			if (function.IsUnsafe)
				prefix = "internal unsafe delegate ";
												
			string signature = this.GetSignature(padding + prefix, function, false) + ";";

			return signature;
		}

		public void WriteNativeFunctionsFile(DotNetApiData api, Options options)
		{
			string fileName = Path.Combine(options.OutputPath, options.NativeFunctionsFileName);
			using (StreamWriter file = new StreamWriter(File.Open(fileName, FileMode.Create)))
			{
				file.WriteLine("using System;");
				file.WriteLine("using System.Runtime.InteropServices;");
				file.WriteLine("using System.Text;");
				file.WriteLine();
				file.WriteLine("namespace GLDotNet");
				file.WriteLine("{");

				file.WriteLine("\tinternal static partial class {0}", options.NativeFunctionsClassName);
				file.WriteLine("\t{");

				file.WriteLine("\t\tprivate const string Library = \"opengl32.dll\";");
				file.WriteLine();

				foreach (var function in api.Functions.Where(x => x.IsNative))
				{
					if (function.CanPInvoke)
					{
						file.WriteLine(this.GetNativeDeclaration("\t\t", function, false));
					}
					else
					{
						file.WriteLine(this.GetDelegateDeclaration("\t\t", function));
					}

					file.WriteLine();
				}

				file.WriteLine("\t}");

				file.WriteLine("}");
			}
		}

		private string GetMethodCallParamsList(DotNetFunctionData function)
		{
			string output = string.Empty;

			if (function.Params.Count > 0)
			{
				for (int i = 0; i < function.Params.Count; i++)
				{
					var param = function.Params[i];

					if (param.ShouldUseGenerics)
					{
						output += "(IntPtr)";
					}
                    else if (param.IsEnum)
                    {
                        output += "(uint)";
                    }

					output += param.Name;

					if (param.ShouldUseFixed || param.ShouldUseGenerics)
					{
						output += "Ptr";
					}
					
					if (param.ShouldUseGenerics)
					{
						output += ".AddrOfPinnedObject()";
					}
					
					if (i != function.Params.Count - 1)
						output += ", ";
				}
			}

			return output;
		}

		private string GetDotNetDeclaration(string padding, DotNetFunctionData function, Options options)
		{
			string output = padding + "/// " + function.OriginalName + Environment.NewLine;
			output += padding + "/// Version: " + function.VersionMajor + "." + function.VersionMinor + Environment.NewLine;
							
			output += padding + GetSignature("public ", function, true);

			output += Environment.NewLine + padding + "{" + Environment.NewLine;

			string bodyPadding = padding + "\t";

			if (function.NativeReturnType != "void")
				output += bodyPadding + function.NativeReturnType + " result;" + Environment.NewLine + Environment.NewLine;

			output += bodyPadding + "this.BeforeNativeFunctionCall(" + options.Prefix.ToUpper() + "NativeFunctions." + function.NativeName + ");" + Environment.NewLine + Environment.NewLine;

            if (function.Params.Any(x => x.ShouldUseOut && !x.ShouldUseFixed))
            {
                foreach (var param in function.Params.Where(x => x.ShouldUseOut && !x.ShouldUseFixed))
                {
                    output += bodyPadding + param.Name + " = default(" + param.DotNetType + ");" + Environment.NewLine;
                }

                output += Environment.NewLine;
            }

			bool wrapInTryFinally = function.ShouldUseGenerics;

			if (function.IsUnsafe)
			{
				output += bodyPadding + "unsafe" + Environment.NewLine;
				output += bodyPadding + "{" + Environment.NewLine;
				bodyPadding += "\t";

				if (function.IsAtLeastOneParamNonVoidPointer)
				{
					foreach (var param in function.Params)
					{
						if (param.ShouldUseFixed)
						{
							output += bodyPadding + "fixed (" + param.NativeType + " " + param.Name + "Ptr = ";

							if (param.ShouldUseAddressOfOperator)
								output += "&";

							output += param.Name + ")" + Environment.NewLine;
						}
					}

					output += bodyPadding + "{" + Environment.NewLine;
					bodyPadding += "\t";
				}
			}

			if (wrapInTryFinally)
			{
				foreach (var param in function.Params)
				{
					if (param.ShouldUseGenerics)
						output += bodyPadding + "GCHandle " + param.Name + "Ptr = GCHandle.Alloc(" + param.Name + ", GCHandleType.Pinned);" + Environment.NewLine;
				}

				output += bodyPadding + "try" + Environment.NewLine;
				output += bodyPadding + "{" + Environment.NewLine;
				bodyPadding += "\t";
			}

			if (function.CanPInvoke)
			{
				output += bodyPadding;

				if (function.NativeReturnType != "void")
					output += "result = ";

				if (function.NativeReturnType == "string")
					output += "new string ((sbyte*)";

				output += options.NativeFunctionsClassName + "." + function.NativeName + "(" + this.GetMethodCallParamsList(function);

				if (function.NativeReturnType == "string")
					output += ")";

				output += ");" + Environment.NewLine;
			}
			else
			{
				output += bodyPadding + "if (this._" + function.NativeName + " == null)" + Environment.NewLine;
				output += bodyPadding + "\tthis._" + function.NativeName + " = (" + options.NativeFunctionsClassName + "." + function.NativeName + ")this.LoadExtensionFunction<" + options.NativeFunctionsClassName + "." + function.NativeName + ">();" + Environment.NewLine;
				output += Environment.NewLine;
                                
				output += bodyPadding;

				if (function.NativeReturnType != "void")
					output += "result = ";

				if (function.NativeReturnType == "string")
					output += "new string ((sbyte*)";

				output += "this._" + function.NativeName + "(" + this.GetMethodCallParamsList(function);

				if (function.NativeReturnType == "string")
					output += ")";

				output += ");" + Environment.NewLine;
			}

			if (wrapInTryFinally)
			{
				bodyPadding = bodyPadding.Substring(0, bodyPadding.Length - 1);
				output += bodyPadding + "}" + Environment.NewLine;
				output += bodyPadding + "finally" + Environment.NewLine;
				output += bodyPadding + "{" + Environment.NewLine;

				if (function.ShouldUseGenerics)
				{
					foreach (var param in function.Params)
					{
						if (param.ShouldUseGenerics)
							output += bodyPadding + "\t" + param.Name + "Ptr.Free();" + Environment.NewLine;
					}
				}

				output += bodyPadding + "}" + Environment.NewLine;
			}

			if (function.IsUnsafe)
			{
				if (function.IsAtLeastOneParamNonVoidPointer)
				{
					bodyPadding = bodyPadding.Substring(0, bodyPadding.Length - 1);
					output += bodyPadding + "}" + Environment.NewLine;
				}

				bodyPadding = bodyPadding.Substring(0, bodyPadding.Length - 1);
				output += bodyPadding + "}" + Environment.NewLine;
			}

			output += Environment.NewLine + bodyPadding + "this.AfterNativeFunctionCall(" + options.Prefix.ToUpper() + "NativeFunctions." + function.NativeName + ");" + Environment.NewLine;

			if (function.NativeReturnType != "void")
				output += Environment.NewLine + bodyPadding + "return result;" + Environment.NewLine;

			output += padding + "}";

			return output;
		}

		public void WriteDotNetFunctionsFile(DotNetApiData api, Options options)
		{
			string fileName = Path.Combine(options.OutputPath, options.DotNetFunctionsFileName);
			using (StreamWriter file = new StreamWriter(File.Open(fileName, FileMode.Create)))
			{
				file.WriteLine("using System;");
				file.WriteLine("using System.Runtime.InteropServices;");
				file.WriteLine("using System.Text;");
				file.WriteLine();
				file.WriteLine("namespace GLDotNet");
				file.WriteLine("{");

				file.WriteLine("\tpublic partial class {0}", options.DotNetClassName);
				file.WriteLine("\t{");

				foreach (var function in api.Functions)
				{
					if (!function.CanPInvoke && function.IsNative)
					{
						file.WriteLine("\t\tprivate " + options.NativeFunctionsClassName + "." + function.NativeName + " _" + function.NativeName + ";");
						file.WriteLine();
					}

					file.WriteLine(this.GetDotNetDeclaration("\t\t", function, options));
					file.WriteLine();
				}

				file.WriteLine("\t}");

				file.WriteLine("}");
			}
		}
	}
}
